#include "db_sqlite3.h"
#include "note.h"
#include "keyword.h"
#include "note_filter.h"
#include "keyword_filter.h"
#include "debug.h"
#include "util.h"
#include "unicode.h"

#include "sqlite3.h"

#include <wx/string.h>
#include <wx/strconv.h>

#define wxExc(x) {ErrMsg(L ## x); return false;}
#define SQL_Error(x) {print_SQL_Error(x); return false;}

// see note.h for description of particular fields
static char TABLE_NOTES[] =
        "CREATE TABLE notes ("
        "id INTEGER PRIMARY KEY AUTOINCREMENT, "
        "name TEXT, "                               // TODO: rename to caption
        "keywords TEXT, "
        "manual_keywords TEXT, "
        "date TEXT, "
        "sortdate TEXT, "
        "text TEXT, "
        "created INTEGER, "
        "modified INTEGER"
        ")";

// see keywords.h for description of particular fields
static char TABLE_KEYWORDS[] =
        "CREATE TABLE keywords ("
        "id INTEGER PRIMARY KEY AUTOINCREMENT,"
        "name TEXT,"
        "sortname TEXT,"
        "aliases TEXT,"
        "hints TEXT,"
        "desc TEXT,"
        "created INTEGER,"
        "modified INTEGER,"
        "used INTEGER"
        ")";

static void sqlite3_callback_FindStrI(sqlite3_context *context, int argc, sqlite3_value **argv)
{
    wxASSERT(argc==2);

    // get strings
    const char *cs1 = (const char*)sqlite3_value_text(argv[0]);
    const char *cs2 = (const char*)sqlite3_value_text(argv[1]);

    // empty pattern matches
    if  (*cs2 == '\0') {
        sqlite3_result_int(context, 1);
        return;
    }

    wxString s1(cs1, wxConvUTF8);
    wxString s2(cs2, wxConvUTF8);

    int result=wxString_FindI(s1, s2);
    sqlite3_result_int(context, result != wxNOT_FOUND);
}

bool DBsqlite3::RegisterCallbacks()
{
    wxASSERT(db_handle);
    wxASSERT(connected);

    int res;

    res = sqlite3_create_function(db_handle, "FindStrI", 2, SQLITE_UTF8, 0, &sqlite3_callback_FindStrI, 0, 0);
    if (res != SQLITE_OK)
        SQL_Error("register FindStrI() utf8 callback");

    return true;
}

// only used in debug version
#ifdef _DEBUG
static bool CheckSQLChars(const wxString& str)
{
    static const char* forbidden_chars = "'""\\()&|";
    for (unsigned x=0; x<sizeof(forbidden_chars)/sizeof(char); x++)
        if (str.Find(forbidden_chars[x]) != wxNOT_FOUND)
            return false;
    return true;
}
#endif

DBsqlite3::DBsqlite3()
{
    connected = 0;
    modified = 0;
    db_handle = 0;
}

bool DBsqlite3::isModified() const
{
    return modified;
}

void DBsqlite3::setModified(bool value)
{
    modified = value;
}

void DBsqlite3::print_SQL_Error(wxString desc)
{
    desc.Append(_(": "));
    desc.Append(wxString().FromAscii(sqlite3_errmsg(db_handle)));
    ErrMsg(desc);
}

bool DBsqlite3::CheckFile(const wxString& name)
{
    dbgenter("DBsqlite3::CheckFile(%s)\n", name);
    FILE *f = fopen(name.utf8_str(), "r+");
    if (!f) {
        return false;
    }
    return true;
}

bool DBsqlite3::Create(const wxString& name)
{
    dbgenter("DBsqlite3::Create(%s)\n", name);
    //TODO - change to wxString
    //dbgenter("DBsqlite3::Create(""%s"")\n", name);

    // open database
    if (SQLITE_OK != sqlite3_open(name.utf8_str(), &db_handle)) {
        if (db_handle == 0)
            wxExc("Failed to allocate sqlite3 object");
    }

    // create tables
    if (SQLITE_OK != sqlite3_exec(db_handle, TABLE_NOTES, 0, 0, 0))
        SQL_Error("CREATE TABLE notes");
    if (SQLITE_OK != sqlite3_exec(db_handle, TABLE_KEYWORDS, 0, 0, 0))
        SQL_Error("CREATE TABLE tags");

    connected=1;
    modified=0;

    if (!RegisterCallbacks())
        return false;

    return true;
}

bool DBsqlite3::Connect(const wxString& name)
{
    dbgenter("DBsqlite3::Connect(%s)\n", name);

    //create DB if it doesn't exist
    //TODO: Ask whether to create, redesign code
    if (!CheckFile(name)) {
        return Create(name);
    }

    //TODO - change to wxString
    //dbgenter("DBsqlite3::Connect(""%s"")\n", name);
    if (SQLITE_OK != sqlite3_open(name.utf8_str(), &db_handle)) {
        if (db_handle == 0)
            SQL_Error("Open file");
    }

    // check if all requested tables exists
    char **info;
    int rows, cols;
    if (SQLITE_OK != sqlite3_get_table(db_handle, "SELECT name,sql FROM sqlite_master ORDER BY name", &info, &rows, &cols, 0))
        SQL_Error("SELECT to check DB structure");
    if (rows < 2)
        wxExc("DB structure error: Not enough tables in DB");
    if (strcmp(info[2*2 + 0], "notes"))
        wxExc("DB structure error: table 'notes' not found");
    if (strcmp(info[2*2 + 1], TABLE_NOTES))
        wxExc("DB structure error: table 'notes' has wrong format");
    if (strcmp(info[1*2 + 0], "keywords"))
        wxExc("DB structure error: table 'keywords' not found");
    if (strcmp(info[1*2 + 1], TABLE_KEYWORDS))
        wxExc("DB structure error: table 'keywords' has wrong format");

    sqlite3_free_table(info);

    connected = 1;
    modified=0;

    if (!RegisterCallbacks())
        return false;

    return true;
}

bool DBsqlite3::ValidNoteID(int id)
{
    dbgenter("DBsqlite3::ValidNoteID(%d)\n", id);

    if (!connected)
        wxExc("Database not connected");

    // prepare statement
    char statement[100];
    sprintf(statement, "SELECT id FROM notes WHERE id=%d", id);

    char **info;
    int rows, cols;
    if (SQLITE_OK != sqlite3_get_table(db_handle, statement, &info, &rows, &cols, 0))
        SQL_Error("check ID");
    sqlite3_free_table(info);

    return (rows > 0);
}

bool DBsqlite3::ValidKeywordID(int id)
{
    dbgenter("DBsqlite3::ValidKeywordID(%d)\n", id);

    if (!connected)
        wxExc("Database not connected");

    // prepare statement
    char statement[100];
    sprintf(statement, "SELECT id FROM keywords WHERE id=%d", id);

    char **info;
    int rows, cols;
    if (SQLITE_OK != sqlite3_get_table(db_handle, statement, &info, &rows, &cols, 0))
        SQL_Error("check ID");
    sqlite3_free_table(info);

    return (rows > 0);
}

bool DBsqlite3::GetNoteCount(int *arg_count)
{
    dbgenter("DBsqlite3::GetNoteCount()\n");

    if (!connected)
        wxExc("Database not connected");

    // prepare statement
    char statement[100];
    sprintf(statement, "SELECT Count(*) FROM notes");

    char **info;
    int rows, cols;
    if (SQLITE_OK != sqlite3_get_table(db_handle, statement, &info, &rows, &cols, 0))
        SQL_Error("GetNoteCount");

    int note_count;
    if (1 != sscanf(info[1], "%d", &note_count))
        wxExc("failed to parse note count");
    sqlite3_free_table(info);

    *arg_count = note_count;
    return true;
}

bool DBsqlite3::DeleteNote(int id)
{
    dbgenter("DBsqlite3::DeleteNote(id=%d)\n", id);

    if (!connected)
        wxExc("Database not connected");

    if (!ValidNoteID(id))
        wxExc("Invalid ID");

    // prepare statement
    char statement[100];
    sprintf(statement, "DELETE FROM notes WHERE id=%d", id);

    char **info;
    int rows, cols;
    if (SQLITE_OK != sqlite3_get_table(db_handle, statement, &info, &rows, &cols, 0))
        SQL_Error("delete ID");
    sqlite3_free_table(info);

    modified=1;
    return true;
}

bool DBsqlite3::WriteNewKeyword(Keyword& keyword)
{
    dbgenter("DBsqlite3::WriteNewKeyword('%s')\n", keyword.sortname);
    wxASSERT(keyword.created > 0);
    wxASSERT(keyword.created == keyword.modified);
    wxASSERT(keyword.used >= 0);

    if (!connected)
        wxExc("Database not connected");

    sqlite3_stmt *s;
    dbg("prepare\n");
    if (SQLITE_OK != sqlite3_prepare(db_handle,
        "INSERT INTO keywords (name, sortname, aliases, hints, desc, created, modified, used) "
        "VALUES (?,?,?,?,?,?,?,?)", -1, &s, 0))
        SQL_Error("Prepare INSERT keyword");
    dbg("bind\n");
    if (SQLITE_OK != sqlite3_bind_text(s, 1, keyword.name.utf8_str(), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind name");
    if (SQLITE_OK != sqlite3_bind_text(s, 2, keyword.sortname.utf8_str(), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind sortname");
    if (SQLITE_OK != sqlite3_bind_text(s, 3, keyword.aliases.utf8_str(), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind aliases");
    if (SQLITE_OK != sqlite3_bind_text(s, 4, keyword.hints.utf8_str(), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind hints");
    if (SQLITE_OK != sqlite3_bind_text(s, 5, keyword.desc.utf8_str(), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind desc");
    if (SQLITE_OK != sqlite3_bind_int(s, 6, keyword.created))
        SQL_Error("Bind created");
    if (SQLITE_OK != sqlite3_bind_int(s, 7, keyword.modified))
        SQL_Error("Bind modified");
    if (SQLITE_OK != sqlite3_bind_int(s, 8, keyword.used))
        SQL_Error("Bind used");

    dbg("step\n");
    if (SQLITE_DONE != sqlite3_step(s))
        SQL_Error("Step");
    dbg("finalize\n");
    if (SQLITE_OK != sqlite3_finalize(s))
        SQL_Error("Finalize");
    dbg("done\n");

    // save new keyword ID
    keyword.id = sqlite3_last_insert_rowid(db_handle);

    modified=1;
    return true;
}

// also sets note ID
bool DBsqlite3::WriteNewNote(Note &n)
{
    dbgenter("DBsqlite3::WriteNewNote(%s)\n", n.ToString());
    wxASSERT(n.created > 0);
    wxASSERT(n.created == n.modified);

    if (!connected)
        wxExc("Database not connected");

    wxString sortdate;
    if (!ParseDate(n.date, sortdate))
        wxExc("Unknown date format");

    sqlite3_stmt *s;
    if (SQLITE_OK != sqlite3_prepare(db_handle,
        "INSERT INTO notes (keywords,manual_keywords,date,sortdate,text,created,modified,name) VALUES (?,?,?,?,?,?,?,?)", -1, &s, 0))
        SQL_Error("Prepare INSERT note");
    if (SQLITE_OK != sqlite3_bind_text(s, 1, n.ParseAllKeywords().mb_str(wxConvUTF8), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind keywords");
    if (SQLITE_OK != sqlite3_bind_text(s, 2, n.manual_keywords.mb_str(wxConvUTF8), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind manual keywords");
    if (SQLITE_OK != sqlite3_bind_text(s, 3, n.date.mb_str(wxConvUTF8), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind date");
    if (SQLITE_OK != sqlite3_bind_text(s, 4, sortdate.mb_str(wxConvUTF8), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind sortdate");
    if (SQLITE_OK != sqlite3_bind_text(s, 5, n.text.mb_str(wxConvUTF8), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind text");
    if (SQLITE_OK != sqlite3_bind_int(s, 6, n.created))
        SQL_Error("Bind created");
    if (SQLITE_OK != sqlite3_bind_int(s, 7, n.modified))
        SQL_Error("Bind modified");
    if (SQLITE_OK != sqlite3_bind_text(s, 8, n.caption.utf8_str(), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind caption");

    if (SQLITE_DONE != sqlite3_step(s))
        SQL_Error("Step");
    if (SQLITE_OK != sqlite3_finalize(s))
        SQL_Error("Finalize");

    // save new note ID
    n.id = sqlite3_last_insert_rowid(db_handle);

    // DB was modified
    modified=1;

    return true;
}

bool DBsqlite3::OverwriteNote(const Note& n)
{
    dbgenter("DBsqlite3::OverwriteNote(%s)\n", n.ToString());
    wxASSERT(n.created > 0);
    wxASSERT(n.created < n.modified);

    wxString sortdate;

    if (!connected)
        wxExc("Database not connected");

    if (!ParseDate(n.date, sortdate))
        wxExc("Unknown date format");

    if (!ValidNoteID(n.id))
        wxExc("Internal Error: Invalid note id");

    sqlite3_stmt *s;
    if (SQLITE_OK != sqlite3_prepare(db_handle,
        "UPDATE notes SET keywords=?, manual_keywords=?, date=?, sortdate=?, text=?, created=?, modified=?, name=? "
        "WHERE id=?", -1, &s, 0))
        SQL_Error("Prepare INSERT note");
    if (SQLITE_OK != sqlite3_bind_text(s, 1, n.ParseAllKeywords().mb_str(wxConvUTF8), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind keywords");
    if (SQLITE_OK != sqlite3_bind_text(s, 2, n.manual_keywords.mb_str(wxConvUTF8), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind manual keywords");
    if (SQLITE_OK != sqlite3_bind_text(s, 3, n.date.mb_str(wxConvUTF8), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind date");
    if (SQLITE_OK != sqlite3_bind_text(s, 4, sortdate.mb_str(wxConvUTF8), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind sortdate");
    if (SQLITE_OK != sqlite3_bind_text(s, 5, n.text.mb_str(wxConvUTF8), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind note");
    if (SQLITE_OK != sqlite3_bind_int(s, 6, n.created))
        SQL_Error("Bind created");
    if (SQLITE_OK != sqlite3_bind_int(s, 7, n.modified))
        SQL_Error("Bind modified");
    if (SQLITE_OK != sqlite3_bind_text(s, 8, n.caption.utf8_str(), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind caption");
    if (SQLITE_OK != sqlite3_bind_int(s, 9, n.id))
        SQL_Error("Bind ID");

    if (SQLITE_DONE != sqlite3_step(s))
        SQL_Error("Step");
    if (SQLITE_OK != sqlite3_finalize(s))
        SQL_Error("Finalize");

    // DB was modified
    modified=1;

    return true;
}

wxString sqlite3_column_wxstring(sqlite3_stmt *stmt, int col)
{
    if (sqlite3_column_type(stmt, col) == SQLITE_NULL)
        return wxEmptyString;
    wxASSERT(sqlite3_column_type(stmt, col) == SQLITE_TEXT);
    return wxString::FromUTF8((const char*)sqlite3_column_text(stmt, col));
}

// returns number of notes selected
bool DBsqlite3::SelectByFilter(std::vector<Note> *notes, const NoteFilter& filter)
{
    dbgenter("DBsqlite3::SelectByFilter(%s)\n", filter.ToString());

    if (!connected)
        wxExc("Database not connected");

    notes->clear();

    // get first 2 keywords
    wxString keyword1 = wxEmptyString;
    wxString keyword2 = wxEmptyString;
    if (!filter.keywords.empty())
        keyword1 = filter.keywords[0];
    if (filter.keywords.size() >= 2)
        keyword2 = filter.keywords[1];

    // fix ToDate
    wxString from_date = filter.from_date;
    wxString to_date = filter.to_date.IsEmpty() ? L"9999-99-99" : filter.to_date;
    ParseDate(from_date, from_date);
    ParseDate(to_date, to_date);

    // only some of filters get into SQL statement
    wxString statement =
            L"SELECT id, manual_keywords, date, text, created, modified, name FROM notes WHERE "
            L"FindStrI(keywords, $1) AND FindStrI(keywords, $2) AND (FindStrI(text, $3) OR FindStrI(name, $3)) "
            L"ORDER BY sortdate";
    sqlite3_stmt* stmt;
    const unsigned max_statement_length=1000;
    wxASSERT(strlen(statement.utf8_str()) < max_statement_length);
    if (SQLITE_OK != sqlite3_prepare_v2(db_handle, statement.utf8_str(), max_statement_length, &stmt, 0))
        SQL_Error("sqlite3_prepare_v2() failed");
    if (SQLITE_OK != sqlite3_bind_text(stmt, 1, keyword1.utf8_str(), -1, SQLITE_TRANSIENT))
        SQL_Error("sqlite3_bind(1) failed");
    if (SQLITE_OK != sqlite3_bind_text(stmt, 2, keyword2.utf8_str(), -1, SQLITE_TRANSIENT))
        SQL_Error("sqlite3_bind(2) failed");
    if (SQLITE_OK != sqlite3_bind_text(stmt, 3, filter.body.utf8_str(), -1, SQLITE_TRANSIENT))
        SQL_Error("sqlite3_bind(3) failed");

    int notes_found=0;
    while(1) {
        int sqlite_ret = sqlite3_step(stmt);
        if (sqlite_ret == SQLITE_DONE)
            break;
        if (sqlite_ret != SQLITE_ROW)
            SQL_Error("sqlite3_step() returned unexpected value");

        // load note
        Note n;
        wxASSERT(sqlite3_column_count(stmt) == 7);
        wxASSERT(sqlite3_column_type(stmt, 0) == SQLITE_INTEGER);
        wxASSERT(sqlite3_column_type(stmt, 4) == SQLITE_INTEGER);
        wxASSERT(sqlite3_column_type(stmt, 5) == SQLITE_INTEGER);
        n.id              = sqlite3_column_int(stmt, 0);
        n.manual_keywords = sqlite3_column_wxstring(stmt, 1);
        n.date            = sqlite3_column_wxstring(stmt, 2);
        n.text            = sqlite3_column_wxstring(stmt, 3);
        n.created         = sqlite3_column_int(stmt, 4);
        n.modified        = sqlite3_column_int(stmt, 5);
        n.caption         = sqlite3_column_wxstring(stmt, 6);

        // filter note fully
        if (!filter.Matches(n))
            continue;

        // warn about incorrect notes in DB
        if (!CheckKeywordsString(n.manual_keywords)) {
            ErrMsg(_("Error in database.\nInvalid manual keywords in note #%d, date='%s'\n\n%s"), n.id, n.date, n.manual_keywords);
        }

        notes->push_back(n);
        notes_found++;

        if (notes_found >= filter.max_notes) {
            wxString warnmsg = wxString::Format(_("More than %ld results found.\nExtra results not displayed."), filter.max_notes);
            wxMessageBox(warnmsg, _("Warning"));
            break;
        }
    }

    sqlite3_finalize(stmt);

    return true;
}

bool DBsqlite3::ReadKeywords(std::vector<Keyword> *keywords, const KeywordFilter& filter)
{
    dbgenter("DBsqlite3::ReadKeywords()\n");
    int x, rows, cols;
    char **info;

    if (!connected)
        wxExc("Database not connected");

    keywords->clear();

    // TODO - use bind
    //wxASSERT(CheckSQLChars(filter.name));
    wxString statement = wxString::Format(
            "SELECT id, name, sortname, aliases, hints, desc, created, modified, used"
            " FROM keywords"
            " WHERE FindStrI(name, '%s')"
            " ORDER BY sortname",
            filter.name
            );
    if (SQLITE_OK != sqlite3_get_table(db_handle, statement.utf8_str(), &info, &rows, &cols, 0))
        SQL_Error("reading keywords");
    cols = 9;

    for (x=1; x<=rows; x++) {
        Keyword t;

        if (sscanf(info[cols*x], "%u", &t.id) == 0)
            wxExc("Failed to parse ID");

        t.name      = wxString().FromUTF8(info[cols*x + 1]);
        t.sortname  = wxString().FromUTF8(info[cols*x + 2]);
        t.aliases   = wxString().FromUTF8(info[cols*x + 3]);
        t.hints     = wxString().FromUTF8(info[cols*x + 4]);
        t.desc      = wxString().FromUTF8(info[cols*x + 5]);

        if (sscanf(info[cols*x + 6], "%ld", &t.created) == 0)
            wxExc("Failed to parse creation time");
        if (sscanf(info[cols*x + 7], "%ld", &t.modified) == 0)
            wxExc("Failed to parse last modification time");
        if (sscanf(info[cols*x + 8], "%ld", &t.used) == 0)
            wxExc("Failed to parse last usage time");

        wxASSERT(t.created <= t.modified);
        wxASSERT(t.modified <= t.used);

//        if (sscanf(info[3*x+2], "%u", &t.type) == 0)
//            wxExc("Failed to parse type");

        keywords->push_back(t);
    }

    sqlite3_free_table(info);

    return true;
}


void DBsqlite3::Disconnect()
{
    dbgenter("DBsqlite3::Disconnect()\n");

    dbg("sqlite3_close retuned %d\n", sqlite3_close(db_handle)) ;
    connected = 0;
}

bool DBsqlite3::isConnected() const
{
    return connected;
}

// TODO: make this run asynchronously, so that it doesn't stop NoteDialog::OnMarkKeyword
bool DBsqlite3::OverwriteKeyword(const Keyword& k)
{
    dbgenter("DBsqlite3::OverwriteKeyword(%s)\n", k.ToString());
    wxASSERT(k.created > 0);
    wxASSERT(k.created <= k.modified);      // created==modified when keyword last-used timestamp is updated
    wxASSERT(k.modified <= k.used);

    wxString sortdate;

    if (!connected)
        wxExc("Database not connected");

    if (!ValidKeywordID(k.id))
        wxExc("Internal Error: Invalid note id");

    sqlite3_stmt *s;
    if (SQLITE_OK != sqlite3_prepare(db_handle,
        "UPDATE keywords SET name=?, sortname=?, aliases=?, hints=?, desc=?, created=?, modified=?, used=? "
        "WHERE id=?", -1, &s, 0))
        SQL_Error("Prepare INSERT note");
    if (SQLITE_OK != sqlite3_bind_text(s, 1, k.name.utf8_str(), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind name");
    if (SQLITE_OK != sqlite3_bind_text(s, 2, k.sortname.utf8_str(), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind sortname");
    if (SQLITE_OK != sqlite3_bind_text(s, 3, k.aliases.utf8_str(), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind aliases");
    if (SQLITE_OK != sqlite3_bind_text(s, 4, k.hints.utf8_str(), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind hints");
    if (SQLITE_OK != sqlite3_bind_text(s, 5, k.desc.utf8_str(), -1, SQLITE_TRANSIENT))
        SQL_Error("Bind desc");
    if (SQLITE_OK != sqlite3_bind_int(s, 6, k.created))
        SQL_Error("Bind created");
    if (SQLITE_OK != sqlite3_bind_int(s, 7, k.modified))
        SQL_Error("Bind modified");
    if (SQLITE_OK != sqlite3_bind_int(s, 8, k.used))
        SQL_Error("Bind used");
    if (SQLITE_OK != sqlite3_bind_int(s, 9, k.id))
        SQL_Error("Bind ID");

    if (SQLITE_DONE != sqlite3_step(s))
        SQL_Error("Step");
    if (SQLITE_OK != sqlite3_finalize(s))
        SQL_Error("Finalize");

    // DB was modified
    modified=1;

    return true;
}

bool DBsqlite3::DeleteKeyword(int id)
{
    dbgenter("DBsqlite3::DeleteKeyword(id=%d)\n", id);

    if (!connected)
        wxExc("Database not connected");

    if (!ValidKeywordID(id))
        wxExc("Invalid ID");
    wxASSERT(id >= 0);  // >0 ?

    // prepare statement
    char statement[100];
    sprintf(statement, "DELETE FROM keywords WHERE id=%d", id);

    char **info;
    int rows, cols;
    if (SQLITE_OK != sqlite3_get_table(db_handle, statement, &info, &rows, &cols, 0))
        SQL_Error("delete ID");
    sqlite3_free_table(info);

    modified=1;

    return true;
}
